iT邦幫忙

2025 iThome 鐵人賽

DAY 11
0
Vue.js

Vue3.6 的革新:深入理解 Composition API系列 第 11

Day 11: 跨元件的 provide / inject

  • 分享至 

  • xImage
  •  

資料傳遞的方法 props / emit / v-model 明顯看到主要是以一層傳遞一層的方法執行,但是如果要多層怎麼辦呢?
Props

這時候就要靠 provide / inject 依賴注入
provide / inject

provide / inject 是什麼(核心觀念)


  • 目的:讓「任一祖先元件」把資料或服務注入到「任一深度的子孫元件」,而不需要層層用 props 傳下去。
  • 運作
    1. 祖先:provide(key, value) 註冊一個鍵值。
    2. 子孫:inject(key) 取到同一個 value同一個引用/引用型別,不是複製品)。
  • 反應性provide() 本身不會讓值變反應式你提供的值要自己是 refreactive,子孫才會跟著更新。
  • 解析時機inject() 在子孫建立時解析,目前拿到的是祖先「當時」提供的那個引用。之後的變化要靠「引用內的值改變」來傳遞(例如改 ref.value),而不是再呼叫一次 provide()

簡圖(方向:資料/服務向下游傳遞):

Root -- provide(key → value/ref/reactive) --> Child --> Grandchild (inject(key))

何時該用 provide / inject


  • 跨層級的「上下文/服務」:主題(theme)、i18n、表單群組、步驟容器、Modal/Toast 管理器、路由/驗證等「像插件一樣的服務」。
  • 想降低耦合:子孫只知道「我需要這個服務的介面」,不知道是誰提供的。
  • 需要在子孫多處共享同一份狀態或方法。

不適合:只是單純的「父傳子」「子回父」日常資料流,這時用 props / emits 更直覺且可追蹤。

基本用法(安全、可維護、可型別)


步驟一:宣告 Injection Key

import type { InjectionKey, Ref } from 'vue'

export interface CounterCtx {
  count: Ref<number>
  inc: () => void
}

export const CounterKey: InjectionKey<CounterCtx> = Symbol('Counter')

步驟二:祖先提供(Provider)

<script setup lang="ts">
import { ref, provide, readonly } from 'vue'
import { CounterKey } from './keys'

const count = ref(0)
const inc = () => { count.value++ }

provide(CounterKey, { count: readonly(count), inc }) // 提供「可讀 readonly + 方法」,避免子孫直接改 count
</script>

<template>
  <slot />
</template>

步驟三:子孫注入(Consumer)

<script setup lang="ts">
import { inject } from 'vue'
import { CounterKey } from './keys'

const ctx = inject(CounterKey)
if (!ctx) throw new Error('Counter provider not found')

function onClick() {
  ctx.inc()
}
</script>

<template>
  <button @click="onClick">+1</button>
  <p>count: {{ ctx.count }}</p>
</template>

示意圖:
把以上邏輯用畫圖示意

範例 - FormGroup 與 FormItem


在表單開發中,常見的需求是讓「表單群組 (FormGroup)」集中管理資料,並讓「表單項目 (FormItem)」能直接讀取或更新這份資料。如果單純依靠 propsemit,每一層元件都需要手動傳遞資料,既繁瑣又容易出錯。

Vue 提供的 provide / inject 模式正好能解決這個問題。

FormGroup 作為提供者 (Provider),建立並管理一個共享的 model,再透過 provide 將它傳遞給子孫元件。

/** FormGroup(Provider)*/
<script setup lang="ts">
import { reactive, provide, readonly, type InjectionKey } from 'vue'

interface FormCtx {
  model: Record<string, any>
  update: (name: string, val: any) => void
}

// 使用 Symbol 作為 provide/inject 的唯一 key,並且型別化 (InjectionKey<FormCtx>)
export const FormKey: InjectionKey<FormCtx> = Symbol('Form')

const model = reactive({}) // 全組共享
function update(name: string, val: any){ model[name] = val }

// 把 model 和 update 提供給子元件,並透過 readonly(model) → 保證子層不能直接改資料,只能透過 update
provide(FormKey, { model: readonly(model), update })
</script>

<template><slot /></template>

FormItem 作為消費者 (Consumer),無論嵌套在幾層內部,都能透過 inject 直接取得 model 和更新方法。

/** FormItem(Consumer)*/
<script setup lang="ts">
import { inject, toRef, watch } from 'vue'
import { FormKey } from './FormGroup.vue'

const props = defineProps<{ name: string; modelValue?: any }>()
const emit  = defineEmits<{ (e:'update:modelValue', v:any): void }>()

// 透過 inject(FormKey) 連接到 FormGroup 提供的 model 與 update
const form  = inject(FormKey)
if (!form) throw new Error('FormGroup missing')

watch(() => props.modelValue, v => form.update(props.name, v))
</script>

<template>
  <input
    :value="form.model[props.name]"
    @input="form.update(props.name, $event.target.value)"
  />
</template>

最終,FormGroup 只需要用 <slot> 包裹子元件,整個表單的結構就能保持乾淨,同時所有 FormItem 也能共享並同步這份資料。

<FormGroup>
  <FormItem name="username" />
  <FormItem name="email" />
</FormGroup>

參考資料


  1. Vue.js - Provide / Inject

上一篇
Day 10: 單向數據流 props / emit / v-model 的關鍵概念
下一篇
Day 12: v-model 的進化
系列文
Vue3.6 的革新:深入理解 Composition API12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言